import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.Queue;

import javax.sound.sampled.AudioFormat;
import javax.swing.*;

public class AsteroidsGame extends JFrame implements KeyListener,Runnable
{	
	public static int TITLE_BAR_SIZE;
	
	public static final int SCREEN_WIDTH = 800;
	public static final int SCREEN_HEIGHT = 640;
	
	public static final int GAME_WIDTH = 800;
	public static final int GAME_HEIGHT = 600;
	
	public static final int FRAME_RATE = 50; // frames per second
	
	public static Random rng = new Random();
	
	public static AsteroidsGame game;
	
	public static Font scoreFont; // for drawing "score:", "lives:", and score values
	public static Font levelFont; // for drawing "level xx"
	public static Font textFont; // for drawing large text "paused", "game over"
	public static Font subtextFont; // for drawing general text (eg instructions)
	
	public static final String highScoreFilename = "asteroidScores.txt";
	
	private static final AudioFormat PLAYBACK_FORMAT = new AudioFormat(44100, 16, 1, true, false);

	//public static TestFrame testFrame;
	public static SoundManager soundManager;
	
	public static Sound laser;
	public static Sound ship_explosion;
	public static Sound ship_explosion_crash;
	public static Sound alien_hit;
	public static Sound alien_explosion;
	public static Sound asteroid_explosion;
	public static Sound engine;
	public static Sound whir;
	
	public static void main(String args[])
	{
		game = new AsteroidsGame("Asteroids");
		game.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
		game.setVisible(true);
		
		// figure out how big our title bar is, so that drawing happens properly
		TITLE_BAR_SIZE = game.getInsets().top;
		
		scoreFont = new Font(Font.SANS_SERIF, Font.PLAIN, 16);
		levelFont = new Font(Font.SANS_SERIF, Font.BOLD, 24);
		textFont = new Font(Font.SANS_SERIF, Font.BOLD, 32);
		subtextFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
		
		soundManager = new SoundManager(PLAYBACK_FORMAT, 20);
		laser = soundManager.getSound("laser.wav");
		ship_explosion = soundManager.getSound("ship_explosion.wav");
		ship_explosion_crash = soundManager.getSound("ship_explosion_crash.wav");
		alien_hit = soundManager.getSound("asteroid_explosion.wav");
		alien_explosion = soundManager.getSound("alien_kill.wav");
		asteroid_explosion = soundManager.getSound("asteroid_explosion.wav");
		//engine = soundManager.getSound("ship_engine.wav");
		whir = soundManager.getSound("whir.wav");
		
		Thread thread = new Thread(game);
		thread.start();
	}
	
	// minimum distance from a player that new objects can appear
	// can't be much bigger than a quarter of the screen width,
	//  or the game will go into an infinite loop trying to place objects
	static public final double PLAYER_BUFFER = 150;
	
	// asteroid count increases by 1 each level, until the maximum is reached
	static public final int ASTEROIDS_MAX = 15;
	
	static public final int PLAYER_LIVES = 3;
	
	static public enum Mode { RUNNING, PAUSED, GAMEOVER, OPTIONS, HIGHSCORE };
	public Mode mode;
	public boolean quitting = false;
	
	public Options options;
	
	public Spaceship player1;
	public Spaceship player2;
	public GravitationalObject planet;
	
	private List<SpaceObject> gameObjects = new ArrayList<SpaceObject>();
	
	// holds objects needing to be added to or deleted from gameObjects
	private Queue<SpaceObject> addQueue = new ArrayDeque<SpaceObject>();
	private Queue<SpaceObject> deleteQueue = new ArrayDeque<SpaceObject>();
	
	public int level;
	public int score1;
	public int score2;
	
	public int lives1;
	public int lives2;
	
	public boolean asteroidsLeft;
	
	public int highScoreEntryLine;
	public int highScoreEntryLine2;
	public int highScoreEntryChar;
	public String[] highscoreNames = new String[10];
	public int[] highscores = new int[10];
	
	public boolean p1NeedsName = false;
	public boolean p2NeedsName = false;
	
	public AsteroidsGame(String title)
	{
		super(title);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		addKeyListener(this);
		
		options = new Options();
		
		newGame();
		
		// read high score file; since both scores are 0, nothing will be updated
		newHighScores(false); 
	}
	
	public void paint(Graphics screenGraphics)
	{		
		Image buffer = createImage(SCREEN_WIDTH, SCREEN_HEIGHT);
		Graphics g = buffer.getGraphics();
		g.translate(0, TITLE_BAR_SIZE);
		
		
		if(mode == Mode.RUNNING || mode == Mode.PAUSED || mode == Mode.GAMEOVER || mode == Mode.OPTIONS ||
				mode == Mode.HIGHSCORE)
		{
    		g.setColor(Color.BLACK);
    		g.fillRect(0,0, SCREEN_WIDTH, SCREEN_HEIGHT);
    		
    		// draw all the game objects over the black background
    		g.translate(0, SCREEN_HEIGHT - GAME_HEIGHT);
    		synchronized(gameObjects)
    		{
    			for(SpaceObject o : gameObjects)
    				drawObject(o, g);
    		}
    		g.translate(0, -(SCREEN_HEIGHT - GAME_HEIGHT));
    		
    		// draw the status area above the game
    		g.setColor(Color.BLACK);
    		g.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT - GAME_HEIGHT);
    		g.setColor(Color.WHITE);
    		g.drawLine(0, SCREEN_HEIGHT - GAME_HEIGHT - 1, SCREEN_WIDTH - 1, SCREEN_HEIGHT - GAME_HEIGHT - 1);
    		
    		drawStatus(g);
		}
		
		if(mode == Mode.PAUSED)
    		drawBoxText(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, "Paused", "Press P to resume.", g);
		if(mode == Mode.GAMEOVER || mode == Mode.HIGHSCORE)
			drawHighScores(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, g);
		
		if(mode == Mode.OPTIONS)
			options.drawOptionsMenu(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, g);
		
		screenGraphics.drawImage(buffer, 0, 0, this);
		g.dispose();
	}
	
	public void keyPressed(KeyEvent e)
	{
		if(mode == Mode.RUNNING)
		{
    		player1.keyPress(e);
    		if(player2 != null)
    			player2.keyPress(e);
    		
    		if(e.getKeyCode() == KeyEvent.VK_P)
    			mode = Mode.PAUSED;
    		if(e.getKeyCode() == KeyEvent.VK_ESCAPE)
    		{
    			options.startOptions();
    			mode = Mode.OPTIONS;
    		}
		}
		else if(mode == Mode.PAUSED)
		{
			if(e.getKeyCode() == KeyEvent.VK_P)
				mode = Mode.RUNNING;
		}
		else if(mode == Mode.GAMEOVER)
		{
			if(e.getKeyCode() == KeyEvent.VK_ENTER)
				newGame();
		}
		else if(mode == Mode.OPTIONS)
		{
			options.keyPress(e);
		}
		else if(mode == Mode.HIGHSCORE)
		{
			if(e.getKeyCode() == KeyEvent.VK_ESCAPE || e.getKeyCode() == KeyEvent.VK_ENTER)
			{
				if(p1NeedsName)
					p1NeedsName = false;
				else if(p2NeedsName)
					p2NeedsName = false;
			}
			
			char[] newname = highscoreNames[highScoreEntryLine].toCharArray();
			newname[highScoreEntryChar] = e.getKeyChar();
			highscoreNames[highScoreEntryLine] = new String(newname);
			
			if(newname[highScoreEntryChar] != KeyEvent.CHAR_UNDEFINED)
				highScoreEntryChar++;
			if(highScoreEntryChar == 5)
			{
    			if(p1NeedsName)
    			{
    				p1NeedsName = false;
    				highScoreEntryChar = 0;
    				if(p2NeedsName)
    					highScoreEntryLine = highScoreEntryLine2;
    			}
    			else if(p2NeedsName)
    			{
    				p2NeedsName = false;
    			}
			}
		}
		e.consume();
	}
	
	public void keyReleased(KeyEvent e) 
	{
		if(mode == Mode.RUNNING)
		{
    		player1.keyRelease(e);
    		if(player2 != null)
    			player2.keyRelease(e);
		}
		e.consume();
	}
	
	public void keyTyped(KeyEvent e) {}
	
	public void run()
	{
		long nextUpdate = 0;
		
		try
		{
    		while(true)
    		{
    			nextUpdate = System.currentTimeMillis() + 1000/FRAME_RATE;
    			
    			if(mode == Mode.RUNNING || mode == Mode.GAMEOVER)
    			{
        			// update objects for any additions or deletions that need to happen
            		synchronized(gameObjects)
            		{	
            			while(!addQueue.isEmpty())
            				gameObjects.add(addQueue.remove());
            			while(!deleteQueue.isEmpty())
            				gameObjects.remove(deleteQueue.remove());
        			}
            		
        			asteroidsLeft = false;
    				synchronized(gameObjects)
    				{
    					// update the position of all objects; also get an update on asteroid status
            			for(SpaceObject o : gameObjects)
            			{
            				if(o instanceof Asteroid)
            					asteroidsLeft = true;
            				if(o instanceof MovingObject)
            					((MovingObject)o).move();
            			}
            			
            			// check collisions for all pairs of objects
            			checkCollisions();
    				}
    			}
    			
    			if(mode == Mode.RUNNING)
    			{           		
            		if(lives1 <= 0 && (player2 == null || lives2 <= 0))
            		{
            			newHighScores(true);
            		}
            		
            		if(!asteroidsLeft && options.level1Asteroids() > 0)
            			nextLevel();
            		
            		if(options.aliensOn())
            			createShips();
    			}
    			
    			if(mode == Mode.HIGHSCORE)
    			{
    				if(!p1NeedsName && !p2NeedsName)
    				{
    					writeHighScores();
    					mode = Mode.GAMEOVER;
    				}
    			}
    			
    			if(mode != Mode.HIGHSCORE && quitting)
    				break;
  
            		
        		// redraw screen only if the game logic is running ahead of schedule
        		if(System.currentTimeMillis() < nextUpdate)
        			repaint();
        		  			
    			long sleepTime = nextUpdate - System.currentTimeMillis();
    			Thread.sleep(sleepTime > 0? sleepTime : 0);
    		}
		} catch(InterruptedException e)
		{
			
		}
		finally
		{
			game.dispose();
			System.exit(0);
		}
	}
	
	public void drawStatus(Graphics g)
	{
		// strings to be drawn
		String strScore1 = String.format("Score: %d", score1);
		String strScore2 = String.format("Score: %d", score2);
		String strShips = "Ships:";
		String strLevel = String.format("Level %d", level);
		// dummy string to figure out how much room should be left for the scores
		final String strScoreDummy = "Score: 00000000";
		
		FontMetrics fmScore = g.getFontMetrics(scoreFont);
		FontMetrics fmLevel = g.getFontMetrics(levelFont);
		int scoreWidth = fmScore.stringWidth(strScoreDummy);
		int shipsWidth = fmScore.stringWidth(strShips);
		int levelWidth = fmLevel.stringWidth(strLevel);
		
		int lifeWidth = player1.getLivesImage().getWidth();
		
		int stringHeight = SCREEN_HEIGHT - GAME_HEIGHT - 5;
		int shipHeight = stringHeight - player1.getLivesImage().getHeight();
		
		g.setFont(scoreFont);
		
		g.setColor(Color.RED);
		int x = 10; // current position
		g.drawString(strScore1, x, stringHeight);
		x += scoreWidth + 20;
		if(!options.unlimitedLives())
		{
    		g.drawString(strShips, x, stringHeight);
    		x += shipsWidth + 5;
    		for(int i = 0; i < lives1 - 1; i++)
    		{
    			g.drawImage(player1.getLivesImage(), x, shipHeight, this);
    			x += lifeWidth;
    		}
		}			
		
		if(player2 != null)
		{
			g.setColor(Color.BLUE);
			x = SCREEN_WIDTH - 10 - scoreWidth;
    		
    		g.drawString(strScore2, x, stringHeight);
    		if(!options.unlimitedLives())
    		{
    			x -= 20 + lifeWidth*(lives2 - 1) + shipsWidth;
        		g.drawString(strShips, x, stringHeight);
        		x += shipsWidth + 5;
        		for(int i = 0; i < lives2 - 1; i++)
        		{
        			g.drawImage(player2.getLivesImage(), x, shipHeight, this);
        			x += lifeWidth;
        		}
    		}
		}
		
		if(options.level1Asteroids() > 0)
		{
    		g.setFont(levelFont);
    		g.setColor(Color.WHITE);
    		g.drawString(strLevel, SCREEN_WIDTH/2 - levelWidth/2, stringHeight);
		}
	}
	
	// draw some text in a box, centered at x, y
	public void drawBoxText(int x, int y, String text, String subtext, Graphics g)
	{
		Graphics2D g1 = (Graphics2D) g.create(); // make a copy of the graphics so we can transform it
		g1.translate(x,y);
		
		FontMetrics fmText = g.getFontMetrics(textFont);
		FontMetrics fmSubtext = g.getFontMetrics(subtextFont);
		int textWidth = fmText.stringWidth(text);
		int subtextWidth = fmSubtext.stringWidth(subtext);
		
		int boxWidth = 20 + Math.max(textWidth, subtextWidth); // 10 pixels on either side
		int boxHeight = 20 + fmText.getHeight() + fmSubtext.getHeight(); // 5 pixels above/below, 10 between
		
		g1.setColor(Color.BLACK);
		g1.fillRect(-boxWidth/2, -boxHeight/2, boxWidth, boxHeight);
		g1.setColor(Color.WHITE);
		g1.drawRect(-boxWidth/2, -boxHeight/2, boxWidth, boxHeight);
		
		g1.setFont(textFont);
		g1.drawString(text, -textWidth/2, boxHeight/2 - (15 + fmSubtext.getHeight()));
		g1.setFont(subtextFont);
		g1.drawString(subtext, -subtextWidth/2, boxHeight/2 - 5);
		
	}
	
	// draws an object, using the object's image, centered at its location
	public void drawObject(SpaceObject obj, Graphics g)
	{
		if(obj.getImage() == null)
			return;
		
		Graphics2D g1 = (Graphics2D) g.create(); // make a copy of the graphics so we can transform it
		
		BufferedImage image = obj.getImage();
		int x = (int)obj.getX(); 
		int y = (int)obj.getY();
		
		g1.translate(x, y);
		g1.drawImage(image, -image.getWidth(null)/2,  -image.getHeight(null)/2, null);
		g1.dispose();
	}
	
	public void endOptions(boolean newGame, boolean quit)
	{
		if(quit)
		{
			newHighScores(false);
			quitting = true;
			return;
		}
		else if(newGame)
		{
			// starting a new game this way will not allow high score recording
			newGame();
		}
		else
			mode = Mode.RUNNING;
	}
	
	public void saveGame()
	{
		FileWriter writer = null;
		try
		{
    		final JFileChooser fc = new JFileChooser();
    		int returnVal = fc.showSaveDialog(this);
    		if(returnVal == JFileChooser.APPROVE_OPTION)
    		{
    			File file = fc.getSelectedFile();
    			writer = new FileWriter(file);    			
    			
    			writer.write("asteroids save game\n");
    			writer.write(String.format("%d %d %d %d %d %d \n",
    					level, score1, score2, lives1, lives2, newShipTimer));
    			writer.write(options.saveState());
    			
    			// players need to be saved first so that bullets can load correctly
    			writer.write(player1.saveState());
    			if(player2 != null)
    				writer.write(player2.saveState());
    			
    			
    			synchronized(gameObjects)
    			{
        			for(SpaceObject o : gameObjects)
        			{
        				if(o == player1 || o == player2) // players already saved above
        					continue;
        				writer.write(o.saveState());
        			}
    			}
    			    			
    			writer.close();
    		}
    		
		}
		catch(IOException e)
		{
			JOptionPane.showMessageDialog(this, "Error saving file.");
		}
	}
	
	public void loadGame()
	{
		try
		{
			final JFileChooser fc = new JFileChooser();
			int returnVal = fc.showOpenDialog(this);
    		if(returnVal == JFileChooser.APPROVE_OPTION)
    		{
    			File file = fc.getSelectedFile();
    			if(!file.canRead())
    			{
    				JOptionPane.showMessageDialog(this, "File is unreadable.");
    				return;
    			}
    			BufferedReader reader = new BufferedReader(new FileReader(file));
    			
    			if(!reader.readLine().equals("asteroids save game"))
    			{
    				JOptionPane.showMessageDialog(this, "That is not an Asteroids savefile.");
    				reader.close();
    				return;
    			}
    			
    			// read game line
    			Scanner scanner = new Scanner(reader.readLine());
    			level = scanner.nextInt();
    			score1 = scanner.nextInt();
    			score2 = scanner.nextInt();
    			lives1 = scanner.nextInt();
    			lives2 = scanner.nextInt();
    			newShipTimer = scanner.nextInt();
    			scanner.close();
    			
    			options.loadState(new StringTokenizer(reader.readLine()));
    			
    			synchronized(gameObjects)
    			{
    				gameObjects.clear();

        			String line;
        			while( (line = reader.readLine()) != null)
        			{
        				StringTokenizer strtok = new StringTokenizer(line);
        				switch(strtok.nextToken())
        				{
        				case "AlienShip":
        					gameObjects.add(new AlienShip(strtok, level));
        					break;
        				case "Asteroid":
        					gameObjects.add(new Asteroid(strtok));
        					break;
        				case "Bullet":
        					gameObjects.add(new Bullet(strtok));
        					break;
        				case "GravitationalObject":
        					planet = new GravitationalObject(strtok);
        					gameObjects.add(planet);
        					break;
        				case "Spaceship":
        					Spaceship ship = new Spaceship(strtok);
        					if(ship.getPlayer() == 1)
        						player1 = ship;
        					else if(ship.getPlayer() == 2)
        						player2 = ship;
        					gameObjects.add(ship);
        					break;
        				}
        			}
    			}
    			    			
    			mode = Mode.PAUSED;
    			reader.close();
    		}
		}
		catch(IOException|NumberFormatException e)
		{
			JOptionPane.showMessageDialog(this, "Error loading file -- starting new game.");
			newGame();
		}
	}
	
	public void score(int player, int points)
	{
		if(player == 1)
			score1 += points;
		else if(player == 2)
			score2 += points;
	}
	
	// finds a point on the edge of the screen far enough away from the player
	// for a new asteroid or ship
	public void placeNewObject(MovingObject o)
	{
		do
		{
			// since alien ships always move horiontally, only put them on the sides
			switch(o instanceof AlienShip? 1 : rng.nextInt(2))
			{
			case 0: // top/bottom border
				o.y = -o.getWrapHeight()/2;
				o.x = rng.nextInt(GAME_WIDTH);
				break;
			case 1: // left/right border
				o.y = rng.nextInt(GAME_HEIGHT);
				o.x = -o.getWrapWidth()/2;
				break;
			}
		} while(o.distanceWithWrap(player1) < PLAYER_BUFFER && 
				(player2 == null || o.distanceWithWrap(player2) < PLAYER_BUFFER));
	}
	
	public void newGame()
	{
		level = options.startLevel();
		score1 = score2 = 0;
		lives1 = lives2 = PLAYER_LIVES;
		
		synchronized(gameObjects)
		{
			gameObjects.clear();
			
			player1 = new Spaceship(1);
			gameObjects.add(player1);
			
			if(options.playerTwo())
			{
				player2 = new Spaceship(2);
				gameObjects.add(player2);
			}
			else
				player2 = null;
			
			if(options.planetOn())
			{
				planet = new GravitationalObject(options.planetVisible());
				gameObjects.add(planet);
			}
			else
				planet = null;
		}
		
		// set up time for the first enemy ship, based on starting level
		newShipTimer = 20000 - 1200*Math.min(level, 10);
		newShipTimer *= 0.5 + 0.5*rng.nextDouble() + (0.5 - 0.025*Math.min(level, 20));
			
		if(options.level1Asteroids() > 0)
			createAsteroids();
		mode = Mode.RUNNING;
	}
	
	public void nextLevel()
	{
		lives1 = lives2 = PLAYER_LIVES;
		score(1, 100*level);
		score(2, 100*level);
		level++;
		createAsteroids();
	}
	
	public void createAsteroids()
	{
		int asteroids = Math.min(ASTEROIDS_MAX, options.level1Asteroids() + level - 1);
		for(int i = 0; i < asteroids; i++)
		{
			Asteroid a = new Asteroid(0,0);
			placeNewObject(a);
			addObject(a);
		}
		
	}
	
	int newShipTimer; // ms until next ship, initially 10 seconds
	public void createShips()
	{
		newShipTimer -= 1000/FRAME_RATE;
		if(newShipTimer < 0 )
		{
			// chance of getting a rogue instead of an alien increases with level
			// starts near 0, maxes out at 0.8 around level 10 (asymptotically)
			double rogueChance = 0.8/(1.0 + Math.exp(3.5 - 0.7*level));
			
			ShipObject s;
			if(rng.nextDouble() < rogueChance)
				s = new Spaceship(0);
			else
				s = new AlienShip(0,0, level);
			placeNewObject(s);
			addObject(s);
			
			// time between ships, in ms
			// base time starts at 20 seconds, linearly decreases to 8 seconds at level 10+
			// time is then multiplied by a factor in [1, 1.5) at level 1,
			//  decreases to [0.5, 1) at level 20+
			newShipTimer = 20000 - 1200*Math.min(level, 10);
			newShipTimer *= 0.5 + 0.5*rng.nextDouble() + (0.5 - 0.025*Math.min(level, 20));
		}
	}
	
	// Objects will usually get added while iterating over spaceObjects,
	// so we can't just add them directly
	// instead, this function adds them to a queue, to then be added
	// to spaceObjects at the end of the run() loop
	public void addObject(SpaceObject o)
	{
		addQueue.add(o);
	}
	// similar reason for the queue
	public void deleteObject(SpaceObject o)
	{
		deleteQueue.add(o);
	}
	
	// player obejct is about to get deleted, needs to readded to the game
	public void playerKilled(int player)
	{
		if(player == 1)
		{
			if(!options.unlimitedLives())
				lives1--;
			if(lives1 > 0)
			{
				player1 = new Spaceship(1);
				addObject(player1);
			}
		}
		if(player == 2)
		{
			if(!options.unlimitedLives())
				lives2--;
			if(lives2 > 0)
			{
				player2 = new Spaceship(2);
				addObject(player2);
			}
		}
	}

	
	
	private void checkCollisions()
	{
		for(SpaceObject o : gameObjects)
		{
			for(SpaceObject other : gameObjects)
			{
				if(o instanceof Bullet &&
						(other instanceof Bullet || other instanceof GravitationalObject))
					continue;
				if(o instanceof Asteroid &&
						(other instanceof Asteroid || other instanceof AlienShip || other instanceof GravitationalObject))
					continue;
				if(o instanceof GravitationalObject) // planet is never affected by collisions
					continue;
				if(o instanceof AlienShip &&
						(other instanceof AlienShip || other instanceof Asteroid || other instanceof GravitationalObject))
					continue;
					
				
				if(collidingWith(o, other))
				{
					if(o.hit(other))
						break;
				}
			}
		}
	}
	
	// some geometry functions for collision detection
	public boolean pointInCircle(double px, double py, double cx, double cy, double cr)
	{
		double squareDistance = (px-cx)*(px-cx) + (py-cy)*(py-cy);
		return squareDistance < cr*cr;
	}
	public boolean pointInTriangle(double px, double py, double tx1, double ty1, double tx2,
			double ty2, double tx3, double ty3)
	{
		// math notes: this equation computes the coordinates of the point in a linear
		//  space based on vectors that are two sides of the triangle
		// - those vectors are <tx3-tx2, ty3-ty2> and <tx3-tx1, ty3-ty1>
		// - there is a matrix converting the rectangular coordinates to coordinates in terms
		//    of those two vectors
		// - we invert that matrix to get the new coordinates of the point
		// - then checking some basic bounds of those new coordinates tells us whether
		//    the point is inside the triangle
		// - the determinant will be 0 if the three points do not actually make a triangle,
		//    so hopefully that never happens
		// reference: http://en.wikipedia.org/wiki/Barycentric_coordinate_system
		// sections "Converting to barycentric coordinates" and "Determining whether a point is inside a triangle"
		// 
		// motivation: there are no square roots or cosines anywhere here, so hopefully this is fast enough to do
		//   a whole bunch of times for every object in the game
		
		double det =   (ty2 - ty3)*(tx1 - tx3) + (tx3 - tx2)*(ty1 - ty3);
		double c1 =   ((ty2 - ty3)*(px - tx3)  + (tx3 - tx2)*(py - ty3))/det;
		double c2 =   ((ty3 - ty1)*(px - tx3)  + (tx1 - tx3)*(py - ty3))/det;
		
		return c1 >= 0 && c2 >= 0 && c1 + c2 <= 1;
	}
	public boolean triangleInTriangle(double t1x1, double t1y1, double t1x2, double t1y2, double t1x3, double t1y3,
			double t2x1, double t2y1, double t2x2, double t2y2, double t2x3, double t2y3)
	{
		// check each point of the second triangle against the first triangle,
		// and each point of the first triangle against the second triangle
		// this isn't QUITE accurate, but it's close enough as long as the triangles are small
		return pointInTriangle(t2x1, t2y1, t1x1, t1y1, t1x2, t1y2, t1x3, t1y3) ||
				pointInTriangle(t2x2, t2y2, t1x1, t1y1, t1x2, t1y2, t1x3, t1y3) ||
				pointInTriangle(t2x3, t2y3, t1x1, t1y1, t1x2, t1y2, t1x3, t1y3) ||
				
				pointInTriangle(t1x1, t1y1, t2x1, t2y1, t2x2, t2y2, t2x3, t2y3) ||
				pointInTriangle(t1x2, t1y2, t2x1, t2y1, t2x2, t2y2, t2x3, t2y3) ||
				pointInTriangle(t1x3, t1y3, t2x1, t2y1, t2x2, t2y2, t2x3, t2y3);
	}
	public boolean circleInTriangle(double cx, double cy, double cr, double tx1, double ty1, double tx2,
			double ty2, double tx3, double ty3)
	{
		// check each of the triangles points against the circle, then check the circle's center
		//  against the triangle
		// this isn't quite perfect for a circle that's small compared to the triangle, since it won't
		//  collide until the center enters the triangle; but since case only comes up for small circles
		//  anyway, this is good enough
		return pointInCircle(tx1, ty1, cx, cy, cr) ||
				pointInCircle(tx2, ty2, cx, cy, cr) ||
				pointInCircle(tx3, ty3, cx, cy, cr) ||
				pointInTriangle(cx, cy, tx1, ty1, tx2, ty2, tx3, ty3);
	}
	
	public boolean collidingWith(SpaceObject o1, SpaceObject o2)
	{
		if(o1.collisionType() == SpaceObject.CollisionType.NONE ||
				o2.collisionType() == SpaceObject.CollisionType.NONE)
		{
			return false;
		}
		
		double[] data1 = o1.collisionData(), data2 = o2.collisionData();
		if(o1.collisionType() == SpaceObject.CollisionType.POINT)
		{
			if(o2.collisionType() == SpaceObject.CollisionType.POINT)
				return false; // points will basically never collide with each other
			else if(o2.collisionType() == SpaceObject.CollisionType.TRIANGLE)
			{
				return pointInTriangle(data1[0], data1[1],
						data2[0], data2[1], data2[2], data2[3], data2[4], data2[5]);
			}
			else if(o2.collisionType() == SpaceObject.CollisionType.CIRCLE)
				return pointInCircle(data1[0], data1[1], data2[0], data2[1], data2[2]);
		}
		else if(o1.collisionType() == SpaceObject.CollisionType.TRIANGLE)
		{
			if(o2.collisionType() == SpaceObject.CollisionType.POINT)
			{
				return pointInTriangle(data2[0], data2[1],
						data1[0], data1[1], data1[2], data1[3], data1[4], data1[5]);
			}
			else if(o2.collisionType() == SpaceObject.CollisionType.TRIANGLE)
			{
				return triangleInTriangle(data1[0], data1[1], data1[2], data1[3], data1[4], data1[5],
						data2[0], data2[1], data2[2], data2[3], data2[4], data2[5]);
			}
			else if(o2.collisionType() == SpaceObject.CollisionType.CIRCLE)
			{
				return circleInTriangle(data2[0], data2[1], data2[2],
						data1[0], data1[1], data1[2], data1[3], data1[4], data1[5]);
			}
		}
		else if(o1.collisionType() == SpaceObject.CollisionType.CIRCLE)
		{
			if(o2.collisionType() == SpaceObject.CollisionType.POINT)
				return pointInCircle(data2[0], data2[1], data1[0], data1[1], data1[2]);
			else if(o2.collisionType() == SpaceObject.CollisionType.TRIANGLE)
			{
				return circleInTriangle(data1[0], data1[1], data1[2],
						data2[0], data2[1], data2[2], data2[3], data2[4], data2[5]);
			}
			else if(o2.collisionType() == SpaceObject.CollisionType.CIRCLE)
				return false; // with our game logic, two circular objects never need to collide
		}
		
		System.err.printf("collision between objects with types %s, %s?\n",
				o1.collisionType(), o2.collisionType());
		return false;
	}
	
	public void newHighScores(boolean keepDisplay)
	{
		// get filename
		URL dir = getClass().getProtectionDomain().getCodeSource().getLocation();
		File hiscoreFile = null;
		hiscoreFile = new File(dir.getPath() + highScoreFilename);
		
		
		Scanner scanner;
		try
		{
			scanner = new Scanner(hiscoreFile);
		}
		catch(FileNotFoundException e)
		{
			createDefaultScoreFile(hiscoreFile);
			// now try again
			try
			{
				scanner = new Scanner(hiscoreFile);
			}
			catch(FileNotFoundException e2)
			{
				// error message would already have been displayed
				return;
			}
		}
		
		try
		{
			for(int i = 0; i < highscores.length; i++)
    		{
    			highscoreNames[i] = scanner.next();
    			highscores[i] = scanner.nextInt();
    		}
		}
		catch(NoSuchElementException e)
		{
			JOptionPane.showMessageDialog(this, "High score file is corrupt -- resetting it to default.");
			createDefaultScoreFile(hiscoreFile);
			scanner.close();
			return;
		}
		
		scanner.close();
		
		if(score1 > 0)
			updateScores(score1, 1);
		if(player2 != null && score2 > 0)
			updateScores(score2, 2);
		if(p2NeedsName && !p1NeedsName)
			highScoreEntryLine = highScoreEntryLine2;
		if(!p1NeedsName && !p2NeedsName)
			mode = Mode.GAMEOVER;

		if(!keepDisplay && mode != Mode.HIGHSCORE)
			mode = Mode.RUNNING;
	}
	
	public void updateScores(int newScore, int player)
	{
		int i;
		for(i = 0; i < highscores.length; i++)
		{
			if(newScore > highscores[i])
			{
				for(int j = highscores.length - 2; j >= i; j--)
				{
					highscoreNames[j+1] = highscoreNames[j];
					highscores[j+1] = highscores[j];
				}
				
				mode = Mode.HIGHSCORE;
				if(player == 1)
				{
					highScoreEntryLine = i;
					p1NeedsName = true;
				}
				else if(player == 2)
				{
					highScoreEntryLine2 = i;
					if(i <= highScoreEntryLine)
						highScoreEntryLine += 1;
					p2NeedsName = true;
				}
				highscoreNames[i] = "_____";
				highscores[i] = newScore;
				break;
			}
		}
	}
	
	public void createDefaultScoreFile(File hiscoreFile)
	{
		try
		{
			FileWriter writer = new FileWriter(hiscoreFile);
			writer.write("JACOB 308620\n");
			writer.write("MATT_ 268030\n");
			for(int i = 0; i < highscores.length - 2; i++)
				writer.write("_____ 0\n");
			writer.close();
		}
		catch(IOException e)
		{
			e.printStackTrace();
			JOptionPane.showMessageDialog(this, "Error writing high scores file.");
		}
	}
	
	public void resetScoreFile()
	{
		URL dir = getClass().getProtectionDomain().getCodeSource().getLocation();
		File hiscoreFile = null;
		hiscoreFile = new File(dir.getPath() + highScoreFilename);
		createDefaultScoreFile(hiscoreFile);
	}
	
	// this is based in large part off of the options display
	public void drawHighScores(int boxX, int boxY, Graphics g)
	{
		Graphics2D g1 = (Graphics2D) g.create(); // make a copy of the graphics so we can transform it
		g1.translate(boxX,boxY);
		
		int titleHeight = g1.getFontMetrics(textFont).getHeight();
		int titleWidth = g1.getFontMetrics(textFont).stringWidth("High Scores");
		
		int rowHeight = g1.getFontMetrics(scoreFont).getHeight();
		int nameWidth = g1.getFontMetrics(scoreFont).stringWidth("WWWWW:");
		int scoreWidth = g1.getFontMetrics(scoreFont).stringWidth("00000000");
		
		int boxHeight = 50 + titleHeight + (highscores.length+1)*rowHeight;
		int boxWidth = 100 + nameWidth + scoreWidth; // just an approximation
		
		g1.setColor(new Color(0,0,0,0x80));
		g1.fillRect(-boxWidth/2, -boxHeight/2, boxWidth, boxHeight);
		g1.setColor(Color.WHITE);
		g1.drawRect(-boxWidth/2, -boxHeight/2, boxWidth, boxHeight);
		
		// go back to top left of box
		g1.translate(-boxWidth/2, -boxHeight/2);
		
		int y = 0;
		drawText(boxWidth/2 - titleWidth/2, y, "High Scores", textFont, g1);
		y += titleHeight;
		
		y += rowHeight;
		
		if(mode == Mode.HIGHSCORE)
		{
			drawText(20, y, "Congratulations! Enter your name...", subtextFont, g1);
		}
		else
		{
			drawText(20, y, "Press Enter to start a new game.", subtextFont, g1);
		}		
		y += rowHeight;
		
		for(int i = 0; i < highscores.length; i++)
		{
			if(mode == Mode.HIGHSCORE && highScoreEntryLine == i)
				g1.setColor(Color.YELLOW);
			else
				g1.setColor(Color.WHITE);
			drawText(20, y, hsName(highscoreNames[i]), scoreFont, g1);
			drawText(20 + 50 + nameWidth, y, String.format("%d", highscores[i]), scoreFont, g1);
			y += rowHeight;
		}
	}
	
	private int drawText(int x, int y, String str, Font font, Graphics g)
	{
		Graphics2D g1 = (Graphics2D) g.create();
		g1.translate(x,y);
		
		int height = g1.getFontMetrics(font).getHeight();
		int width = g1.getFontMetrics(font).stringWidth(str);
		
		g1.setFont(font);
		g1.drawString(str, 0, height);
		
		return width;
	}
	
	// converts a name to upper case, with only 5 letters, discarding anything not a letter
	private String hsName(String name)
	{
		char[] dispName = new char[5];
		for(int i = 0; i < name.length(); i++)
		{
			if(Character.isLetter(name.charAt(i)))
				dispName[i] = Character.toUpperCase(name.charAt(i));
			else if(Character.isDigit(name.charAt(i)))
				dispName[i] = name.charAt(i);
			else
				dispName[i] = ' ';
		}
		
		return new String(dispName);
	}
	
	private void writeHighScores()
	{
		URL dir = getClass().getProtectionDomain().getCodeSource().getLocation();
    	File hiscoreFile = null;
    	hiscoreFile = new File(dir.getPath() + highScoreFilename);
		try
		{
			FileWriter writer = new FileWriter(hiscoreFile);
			for(int i = 0; i < highscores.length; i++)
				writer.write(String.format("%s %d\n", highscoreNames[i], highscores[i]));
			writer.close();
		}
		catch(IOException e)
		{
			JOptionPane.showMessageDialog(this, "Error writing high scores file.");
		}
	}
}
